Skip to content

feat(mosaic): wire up user profile emails and phone numbers - #9937

Open
alexcarpenter wants to merge 78 commits into
mainfrom
carp/mosaic-user-profile-email-link-sso
Open

alexcarpenter wants to merge 78 commits into
mainfrom
carp/mosaic-user-profile-email-link-sso

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Description

Wires the email and phone lists in the Mosaic user profile to Clerk, matching the legacy EmailsSection and PhoneSection. On top of #9844. Supersedes #9927 and #9936, which are folded in here.

The diff is large but front-loaded with churn: over half of it is tests, and the production change nets roughly +300 lines, almost all inside features/user-profile/user-profile-account-section/. The reading order below is the short path through it.

Behavior

  • Contacts are ordered primary first, then verified, then pending, then never started, and an unverified one is badged. Users can set one as primary and remove one; the remove dialog warns about losing sign-in only when the contact is verified. An immutable attribute still allows a new primary, but not removal.
  • Adding runs through a controller machine (contact → sending → verify) on useForm, and a contact left pending can be verified from its row menu. The dialog opens on the step for the instance's verification method: code, email link, or enterprise SSO.
  • useForm is now the one owner of pending state and error copy across the section, which retires the per-contact "Unable to set the primary…" strings. The enterprise accounts Connect button becomes a SubmitButton, so it holds its label while the connection runs.

Reading order

File Why
1 user-profile-account-section.types.ts The contract the rest follows: UserProfileEmailVerifier, UserProfilePhoneVerifier, and the code | link | sso verification union.
2 user-profile-account-section.model.ts The only Clerk-aware layer. Builds the verifiers and picks the verification method from the instance's attribute config.
3 user-profile-add-email.controller.ts The machine that drives email → sending → verify off that verifier.
4 user-profile-add-email.dialog.tsx One dialog, four steps (email, verify, link, sso). It absorbs the two standalone verify dialogs this PR deletes.
5 user-profile-add-phone.controller.ts The same shape as 3, minus the link and SSO branches.
6 user-profile-set-primary.controller.ts New, shared by both rows: the pending and error state that used to be per-contact copy.
7 user-profile-email-row.view.tsx, user-profile-phone-row.view.tsx Row menus and badges. Heavy line counts, but the change is which callbacks each action reaches for.
8 user-profile-account-section.utils.ts Contact ordering and the toContactAccess gating that decides what a row may offer.
9 user-profile-account-section.feature.test.tsx The behavior spec for all of the above, against a fake FAPI.

Churn worth skimming rather than reading

  • Two standalone dialogs are gone, folded into step 4 as steps: user-profile-verify-email-link.{dialog,messages,styles} and user-profile-verify-email-sso.{dialog,messages,styles}, plus their two swingset fixtures and their two localization namespaces.
  • The mocked model and integration tests are replaced by the one feature test in step 9: user-profile-account-section.model.test.tsx, three *.integration.test.tsx, and the two verify-dialog tests all come out.
  • user-profile-picture.controller.ts drops a hand-rolled pending/error/in-flight ref for useForm, which is why it shrinks.

Changes outside the section, and why each is here

  • components/form/form.machine.ts — a banner now clears when a submit succeeds rather than when one starts, so a retry does not flash an empty error.
  • utils/form-error.ts — adds toLocalizableError, so a rejection keeps its Clerk error code instead of flattening to a string.
  • blocks/confirmation/confirmation.controller.ts — the remove-contact confirmation localizes the reason the server refused, instead of printing the raw message.
  • components/phone-input/ — exports toCountryIso, so the model can turn clerk.__internal_country into the input's default country.
  • primitives/menu/menu.test.tsx — regression test: the row menu has to hold its items at their last frame while it exits, or "Set as primary" disappears mid-animation after it is clicked.
  • hooks/use-list-removal-focus.ts — exposes trigger(id), so a verify dialog can return focus to the row that opened it.
  • __tests__/feature/fapi.ts, __tests__/feature/fake-fapi.ts — the shared feature-test harness grows attribute overrides, fapiPhoneNumber, and the phone and email verification endpoints.
  • features/user-button/__tests__/user-button.feature.test.tsx — uses that new fapiPhoneNumber helper in place of an inline literal.

Known gaps

  • Link verification redirects to userProfileUrl#/verify. Mosaic has no routing yet, so the base is always the instance's profile URL with a hash path, where legacy derives both from the routing mode, and nothing in Mosaic serves /verify yet. A TODO in the model points at feat(mosaic): add MosaicRoutingProvider and useMosaicRoutes #9843.
  • The SSO step shows the email's domain rather than the designed row per connection with its logo, which the frontend cannot render yet: EmailAddressResource carries only matchesSsoConnection. clerk_go#22625 adds the enterprise_connections it needs.
  • Reverification comes in a follow-up: adding a contact, promoting one to primary, and changing the username are protected actions, and the session's factor verification can be older than they allow.

None of this is exported yet, so the changeset is empty.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1eb657c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Oct 7, 2026 3:02pm UTC
swingset Ready Ready Preview Oct 7, 2026 3:02pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Team

Run ID: 63989e63-5e13-4173-97dc-36c330563144

📥 Commits

Reviewing files that changed from the base of the PR and between f1758aa and 7a674d5.

📒 Files selected for processing (1)
  • packages/mosaic/src/features/user-profile/__tests__/user-profile-account-section.model.test.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

💤 Files with no reviewable changes (1)
  • packages/mosaic/src/features/user-profile/tests/user-profile-account-section.model.test.tsx

Included review availability: This review used your included allowance. 4 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 8 reviews per hour.


📝 Walkthrough

Walkthrough

The account section adds contact access rules and email and phone verification flows using code, link, and SSO methods. It updates form error handling, test API endpoints and fixtures, and profile-picture upload and removal controls. Tests and Swingset stories cover the updated flows, contact ordering, account restrictions, and related UI states.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~50 minutes

Merge Risk: 🟡 Moderate · up to 7a674

The test deletion does not resolve the earlier concerns. A phone verification send failure may show no message. The shared test harness may have duplicate function definitions that block compilation. The fake API can also return stale or colliding data. Resolve these before merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 108 functions across 58 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: connecting Mosaic user-profile email and phone features to Clerk.
Description check ✅ Passed The description explains the contact flows, verification methods, implementation, known gaps, and related changes. It is directly relevant to the pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@alexcarpenter
alexcarpenter changed the base branch from carp/mosaic-user-profile-email-code to carp/mosaic-user-profile-avatar-wireup September 29, 2026 18:30
@alexcarpenter alexcarpenter changed the title feat(mosaic): verify user profile emails by link or enterprise SSO feat(mosaic): wire up user profile emails Sep 29, 2026
@alexcarpenter alexcarpenter changed the title feat(mosaic): wire up user profile emails feat(mosaic): wire up user profile emails and phone numbers Sep 29, 2026
@alexcarpenter
alexcarpenter force-pushed the carp/mosaic-user-profile-avatar-wireup branch from c14b88b to 0f3c4e2 Compare September 29, 2026 18:43
@alexcarpenter
alexcarpenter force-pushed the carp/mosaic-user-profile-email-link-sso branch from c3c548f to 9a7c43d Compare September 29, 2026 18:44
Legacy reads the geo-IP country off the Clerk instance and seeds the phone
input with it. Mosaic's PhoneInput took a defaultCountry but nothing passed
one, so it always started on 'us' and a non-US user typing a national number
submitted a +1 number.

The account section's model now narrows clerk.__internal_country through a
new toCountryIso and threads it down to the input as defaultCountry. The
fake FAPI gained a country seed that serves x-country, so the feature test
exercises the real clerk-js path that populates the value.
The confirmation block rendered the raw rejection message and never reached
the error catalog, so a mapped code like action_blocked lost its copy and a
SaveError built from UNEXPECTED_ERROR surfaced the developer string "Save
failed". Nine views share the block, so all of them showed it.

The machine now holds the LocalizableError and the hook localizes it with
errorText, mirroring how useForm does it. Reading a rejection into a
LocalizableError is the same shape fourteen other sites hand-roll, so it
lands in utils/form-error.ts as toLocalizableError rather than here; the
remaining sites still need migrating.

A plain Error still shows its own message, which keeps working for the
callers that localize before throwing.
The helper clicked the field as soon as it mounted, but the verify step
disables it while the code is being sent and the disabled style sets
pointer-events: none, so on a slow runner the click was refused.
…or it

The SSO step prepared the verification on entry and the Connect button
read the redirect URL off the resource when clicked, so a click that beat
the response found no URL and silently did nothing. Connect now performs
the prepare itself and navigates to the URL it answers with, so there is
nothing to race and a failure is reported instead of swallowed.
A field-scoped prepareVerification failure landed in the form error's fields,
which nothing on the verify step renders, so the user saw an empty error. The
email path already saves without a field list.

Also corrects the swingset add-phone story to the fixture's onCreated/onVerified
contract, and the useForm failure contract to name SaveError.
…ile-email-link-sso

# Conflicts:
#	packages/mosaic/src/features/user-profile/__tests__/user-profile-account-section.integration.test.tsx
#	packages/mosaic/src/features/user-profile/__tests__/user-profile-email-actions.test.tsx
#	packages/mosaic/src/features/user-profile/__tests__/user-profile-phone-actions.test.tsx
…onent

Follows the convention landed in #10076: a feature test lives next to the component it renders.

@Ephem Ephem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just did a quick skim for some obvious stuff.

Comment thread packages/mosaic/src/utils/form-error.ts Outdated
}

/** Reads what any rejection says, keeping a failed save's code so the copy stays localizable. */
export function toLocalizableError(cause: unknown): LocalizableError {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should disappear when you rebase (just commenting so it doesn't accidentally get missed).

Comment on lines +49 to +52
const verification = useUserProfileAddEmailController({
username,
onCreate: onCreateEmail,
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller in a view seems off, plus a bunch of other logic? Same with the phone-row.

Is this some kind of intermediary state that you plan to tackle in a follow up, or how you intended it?

items,
onSetPrimary,
}: UserProfileSetPrimaryControllerOptions): UserProfileSetPrimaryController {
const form = useForm({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use usePendingAction instead now that it exists? Same in user-profile-picture.controller.ts

…ile-email-link-sso

# Conflicts:
#	packages/mosaic/src/blocks/confirmation/confirmation.controller.test.ts
#	packages/mosaic/src/blocks/confirmation/confirmation.controller.ts
#	packages/mosaic/src/features/user-profile/__tests__/user-profile-account-section.model.test.tsx
#	packages/mosaic/src/features/user-profile/__tests__/user-profile-picture.controller.test.ts
#	packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-picture.controller.ts
#	packages/mosaic/src/utils/errors.ts
#	packages/swingset/src/stories/user-profile-account-section.stories.tsx
Give the email and phone rows their own controllers built on usePendingAction,
and delete user-profile-set-primary.controller.ts, which reimplemented that hook
as a single-field form. The picture controller moves onto the same hook.

This branch was successfully deployed

2 active deployments
Preview – swingset — 1eb657c2 Deployed Oct 7, 2026 by vercel[bot]
Preview – clerk-js-sandbox — 1eb657c2 Deployed Oct 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants